<?php

/**
 * Maikuro CMS
 *
 * LICENSE:
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * The license is bundled with this package in the file LICENSE.txt.
 * You may also obtain a copy of the License at this URL:
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @copyright  Copyright (c) 2010, Jonathan Hedrén
 * @author     Jonathan Hedrén <jonathan.hedren@gmail.com>
 * @license    http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 */

/**
 * @see PageEntity
 */
require_once 'entity/PageEntity.php';

/**
 * @see CustomFieldEntity
 */
require_once 'entity/CustomFieldEntity.php';

class PageCacheContainer
{
    private $queryToIdMapping = array();
    
    private $pages = array();

    private $rootPage;

    public function put(PageEntity $page)
    {
        $this->pages[(string) $page->id] = $page;
        return $this;
    }

    public function putRoot(PageEntity $page)
    {
        $this->rootPage = $page;
    }
    
    public function putAll(array $pages, $query)
    {
        $idMappingArray = array();
        foreach ($pages as $page) {
            $this->pages[(string) $page->id] = $page;
            $idMappingArray[] = (string) $page->id;
        }
        $this->queryToIdMapping[$query] = $idMappingArray;
    }
    
    public function get($id)
    {
        return isset($this->pages[$id]) ? $this->pages[$id] : null;
    }

    public function getRoot()
    {
        return $this->rootPage;
    }

    public function getAll($query)
    {   
        if (!isset($this->queryToIdMapping[$query])) {
            return null;
        }
        $result = array();
        foreach ($this->queryToIdMapping[$query] as $pageId) {
            $result[$pageId] = $this->pages[$pageId];
        }
        return $result;
    }
}

/**
 *
 */
class PageService
{

    /**
     *
     * @var ICacheHandler
     */
    private $cacheHandler;

    /**
     *
     * @var string
     */
    private $cachePath;

    /**
     *
     * @var PageCacheContainer
     */
    private $pageCacheContainer;

    /**
     *
     * @var ExtendedPDO
     */
    private $pdo;

    

    /**
     *
     * @param ICacheHandler $cacheHandler
     * @param ExtendedPDO $pdo
     */
    public function __construct(ICacheHandler $cacheHandler, ExtendedPDO $pdo)
    {
        $this->cacheHandler = $cacheHandler;
        $this->pdo = $pdo;
        $this->pageCacheContainer = new PageCacheContainer();
    }

    /**
     *
     * @param array $data
     * @return PageEntity
     */
    public function create(array $data = null)
    {
        $page = new PageEntity();
        $page->created = date('Y-m-d H:i:s');
        $page->updated = date('Y-m-d H:i:s');
        $page->status = isset($data['status']) ? $data['status'] : 3;
        $page->weight = isset($data['weight']) ? $data['weight'] : 0;
        return $page;
    }

    /**
     * Deletes a page entity.
     *
     * @param PageEntity $page
     * @return PageService
     */
    public function delete(PageEntity $page, $deleteDescendants = false)
    {
        $this->pdo->beginTransaction();

        try {
            $descendants = $this->getDescendantsFor($page);

            if ($descendants != null) {
                if ($deleteDescendants) {
                    foreach ($descendants as $descendant) {
                        $this->pdo->delete('page', 'id = ?', $descendant->id);

                        foreach ($this->fetchCustomFields($descendant->id) as $customField) {
                            $this->pdo->delete('custom_field', 'id = ?', $customField->id);
                        }

                        $this->pdo->delete('page_custom_field', 'pageId = ?', $descendant->id);
                        $this->pdo->delete('rewrite', 'id = ?', $descendant->rewriteId);
                    }
                } else {
                    foreach ($descendants as $descendant) {
                        if ($descendant->parentId != $page->id) {
                            continue;
                        }
                        $descendant->parentId = $page->parentId;
                        $this->save($descendant);
                    }
                }
            }

            $this->pdo->delete('page', 'id = ?', $page->id);

            foreach ($this->fetchCustomFields($page->id) as $customField) {
                $this->pdo->delete('custom_field', 'id = ?', $customField->id);
            }

            $this->pdo->delete('page_custom_field', 'pageId = ?', $page->id);
            $this->pdo->delete('rewrite', 'id = ?', $page->rewriteId);
            // $this->eventHandler->dispatchEvent(self::PAGE_ENTITY_DELETE, );

            unset($page);
            
            $this->pdo->commit();
        } catch (PDOException $e) {
            $this->pdo->rollBack();
            throw $e;
        }

        return $this;
    }

    public function getDescendantsFor(PageEntity $page, $setDescendantsOnPage = true)
    {
        $descendants = array();

        $parent = $page;
        while ($this->getChildrenFor($parent) != null) {
            foreach ($parent->children as $child) {
                $descendants[] = $child;
                $parent = $child;
            }
        }

        return $descendants;
    }
    
    /**
     * 
     * @return PDOSelectQuery
     */
    private function getBaseSelectQuery()
    {
        return $this->pdo->select('page.*', 'rewrite.permalink')
                         ->from('page')
                         ->leftJoin('rewrite', 'page.rewriteId = rewrite.id');
    }
    
    public function getChildrenFor(PageEntity $page, $setChildrenOnPage = true)
    {
        $children = $this->pageCacheContainer->getAll('childrenFor-' . $page->id);
        if ($children != null)
        {
            return $children;
        }

        $query = $this->getBaseSelectQuery()
                         ->where('page.parentId = ?');

        switch ($page->childrenSortOrder) {
            case PageEntity::CHILDREN_SORT_ORDER_ALPHABETIC_ASCENDING:
                $query->orderBy('page.title')->asc();
                break;
            case PageEntity::CHILDREN_SORT_ORDER_ALPHABETIC_DESCENDING:
                $query->orderBy('page.title')->desc();
                break;
            case PageEntity::CHILDREN_SORT_ORDER_CHRONOLOGIC_ASCENDING:
                $query->orderBy('page.updated')->asc();
                break;
            case PageEntity::CHILDREN_SORT_ORDER_CHRONOLOGIC_DESCENDING:
                $query->orderBy('page.updated')->desc();
                break;
            default:
                $query->orderBy('page.weight DESC', 'page.title ASC');
                break;
        }
                         
        $children = $query->bindValues($page->id)
                          ->fetchAllAs('PageEntity');

        if (!count($children)) {
            return null;
        }
        
        if ($setChildrenOnPage) {
            $page->children = $children;
        }

        $this->pageCacheContainer->putAll($children, 'childrenFor-' . $page->id);

        return $children;
    }
    
    public function getParentFor(PageEntity $page, $setParentOnPage = true)
    {
        if ($page->parentId == null) {
            return null;
        }

        $parent = $this->pageCacheContainer->get($page->parentId);
        if ($parent != null) {
            return $parent;
        }

        $parent = $this->getBaseSelectQuery()
                       ->where('page.id = ?')
                       ->bindValues($page->parentId)
                       ->fetchAs('PageEntity');

        if ($setParentOnPage) {
            $page->parent = $parent;
        }

        $this->pageCacheContainer->put($parent);

        return $parent;
    }
    
    public function getSiblingsFor(PageEntity $page, $setSiblingsOnPage = true)
    {
        
    }

    public function hasPageChildren($pageId)
    {
        $result = $this->pdo->select('COUNT(*) AS numberOfChildren')
                            ->from('page')
                            ->where('parentId = ?')
                            ->bindValues((int) $pageId)
                            ->fetch();
        return (bool) $result->numberOfChildren;
    }



    /*
    getFirstChildFor
    getLastChildFor
    getNextSiblingFor
    getPreviousSiblingFor
    */

    /**
     * Checks whether $page is an ancestor to the $descendant or not.
     *
     * @param PageEntity $descendant
     * @return boolean
     */
    public function isAncestorTo(PageEntity $page, PageEntity $descendant)
    {
        while ($descendant != null) {
            if ($page->id == $descendant->id) {
                return true;
            }
            $descendant = $this->getPage($descendant->parentId, $descendant->siteId);
        }
        return false;
    }

    /**
     * Checks whether the $descendat is a descendant to the $ancestor.
     *
     * @param PageEntity $descendant
     * @param PageEntity $ancestor
     * @return boolean
     */
    public function isDescendantTo(PageEntity $descendant, PageEntity $ancestor)
    {
        $page = $descendant;
        while ($page !== null) {
            if ($page->id == $ancestor->id) {
                return true;
            }
            $page = $this->getParentFor($page);
        }
        return false;
    }

    
    public function startCacheControl($path)
    {
        $pageCacheContainer = $this->cacheHandler->load($path);
        if ($pageCacheContainer == null) {
            $this->cachePath = $path;
        } else {
            $this->pageCacheContainer = $pageCacheContainer;
        }
        return $this;
    }
    
    public function endCacheControl()
    {
        if ($this->cachePath != null) {
            $this->cacheHandler->save($this->cachePath, $this->pageCacheContainer);
        }
        return $this;
    }
    
    /**
     * Returns the page entity with the specified id.
     *
     * @param integer $pageId
     * @param integer $siteId
     * @return PageEntity
     */
    public function getPage($pageId, $siteId)
    {
        $page = $this->pageCacheContainer->get($pageId);
        if ($page != null) {
            return $page;
        }
        
        $page = $this->pdo->select('page.*', 'rewrite.permalink')
                          ->from('page')
                          ->leftJoin('rewrite', 'page.rewriteId = rewrite.id')
                          ->where('page.siteId = ?')
                          ->andWhere('page.id = ?')
                          ->bindValues($siteId, $pageId)
                          ->fetchAs('PageEntity');
        if ($page == null) {
            return null;
        }

        $this->populateCustomFields($page);
       
        $this->pageCacheContainer->put($page);
        
        return $page;
    }

    /**
     *
     * @param integer $pageId
     * @return array
     */
    private function fetchCustomFields($pageId)
    {
        return $this->pdo->select('id', 'name', 'valueDisplayType', 'value')
                         ->from('custom_field')
                         ->innerJoin('page_custom_field', 'page_custom_field.customFieldId = custom_field.id')
                         ->where('page_custom_field.pageId = ?')
                         ->orderBy('id')
                         ->bindValues($pageId)
                         ->fetchAllAs('CustomFieldEntity');

    }

    /**
     * Returns an array of page entities.
     *
     * @return array
     */
    public function getPages($siteId)
    {
        $result = $this->getBaseSelectQuery()
                       ->where('page.siteId = ?')
                       ->orderBy('weight DESC', 'title ASC')
                       ->bindValues($siteId)
                       ->fetchAllAs('PageEntity');
        foreach ($result as $page) {
            $this->populateCustomFields($page);
        }
        return count($result) ? $result : null;
    }

    /**
     *
     * @param PageEntity $page 
     */
    private function populateCustomFields(PageEntity $page)
    {
        $customFields = $this->fetchCustomFields($page->id);
        foreach ($customFields as $customField) {
            $page->customFields[$customField->name] = $customField;
        }
    }

    /**
     * Returns the root page of the supplied site id.
     *
     * @return PageEntity
     */
    public function getRootPage($siteId)
    {
        $page = $this->pageCacheContainer->getRoot();
        
        if ($page != null) {
            return $page;
        }

        $page = $this->getBaseSelectQuery()
                     ->where('page.siteId = ?')
                     ->andWhere('page.parentId IS NULL')
                     ->bindValues($siteId)
                     ->fetchAs('PageEntity');

        if ($page == null) {
            return null;
        }

        $this->populateCustomFields($page);

        $this->pageCacheContainer->putRoot($page);

        return $page;
    }

    /**
     * Purges the entities cache.
     *
     * @param string $path
     * @return boolean
     */
    public function purgeCache($path)
    {
        return $this->cacheHandler->clean($path);
    }

    /**
     * Saves a page entity.
     *
     * @param PageEntity $page
     * @return PageEntity $page;
     */
    public function save(PageEntity $page)
    {
        $this->pdo->beginTransaction();

        try {
            // Make sure the URL title is set and that it is "URL-friendly".
            if (empty($page->urlTitle)) {
                $page->urlTitle = Filters::urlFriendly($page->title);
            } else {
                $page->urlTitle = Filters::urlFriendly($page->urlTitle);
            }

            $this->savePage($page);
            $this->saveCustomFields($page);
            $this->savePermalink($page);

            // $this->eventHandler->dispatchEvent(self::PAGE_ENTITY_SAVE, $page);

            // ...and update the permalinks of all descendant pages.
            // Fixme: Also update level on all descendants
            $descendants = $this->getDescendantsFor($page);
            if (count($descendants)) {
                foreach ($descendants as $descendant) {
                    $this->savePermalink($descendant);
                }
            }
        } catch (PDOException $e) {
            $this->pdo->rollback();
            throw $e;
        }

        return $page;
    }

    /**
     *
     * @param PageEntity $page
     */
    private function savePage(PageEntity $page)
    {
        $columnsAndValues = $this->getColumnsAndValues("PageEntity", $page);

        // Insert/update the database
        if ($page->id === null) {
            $result = $this->pdo->insert("page", $columnsAndValues);
            // If we've got no affected rows there is a serious problem.
            if ($result < 1) {
                throw new Exception('The database returned zero affected rows which indicates that no data was saved.');
            }
            $page->id = $this->pdo->lastInsertId();
        } else {
            $this->pdo->update("page", $columnsAndValues, 'id = ?', $page->id);
        }
    }

    /**
     *
     * @param PageEntity $page 
     */
    private function saveCustomFields(PageEntity $page)
    {
        $customFieldsToDelete = $this->fetchCustomFields($page->id);

        foreach ($page->customFields as $customField) {
            $columnsAndValues = $this->getColumnsAndValues("CustomFieldEntity", $customField);

            // If there is no id, insert a new row in the database...
            if ($customField->id === null) {
                $result = $this->pdo->insert('custom_field', $columnsAndValues);
                // If we've got no affected row there is a serious problem.
                if ($result < 1) {
                    throw new Exception('The database returned zero affected rows which indicates that no data was saved.');
                }

                $customField->id = $this->pdo->lastInsertId();
                $this->pdo->insert('page_custom_field', array('pageId' => $page->id, 'customFieldId' => $customField->id));
            } else {
                foreach ($customFieldsToDelete as $i => $currentCustomField)
                {
                    if ($currentCustomField->id == $customField->id) {
                        unset($customFieldsToDelete[$i]);
                        break;
                    }
                }

                // ...else update the existing row.
                $result = $this->pdo->update('custom_field', $columnsAndValues, 'id = ?', $customField->id);
            }
        }
        foreach ($customFieldsToDelete as $customFieldToDelete)
        {
            $this->pdo->delete('custom_field', 'id = ?', $customFieldToDelete->id);
            $this->pdo->delete('page_custom_field', 'customFieldId = ?', $customFieldToDelete->id);
        }
    }

    private function getColumnsAndValues($className, $instance)
    {
        // Get all columns and values from the user. Only the properties
        // with the Persistent/PersistentAttribute attribute will be saved.
        $columnsAndValues = array();
        foreach (get_class_vars($className) as $property => $value) {
            $attributes = AttributeParser::getPropertyAttributesFor($className, $property);
            if ($attributes == null || !count($attributes)) {
                continue;
            }
            $persistentAttribute;
            foreach ($attributes as $attribute) {
                if ($attribute instanceof PrimaryKeyAttribute) {
                    continue 2;
                }

                if ($attribute instanceof PersistentAttribute) {
                    $persistentAttribute = $attribute;
                    break;
                }
            }

            if ($persistentAttribute == null) {
                continue;
            }

            $columnsAndValues[$property] = $instance->$property;
        }
        return $columnsAndValues;
    }

    /**
     * Saves the permalink (permanent link/pretty URL/etc) for a page.
     *
     * @param PageEntity $page
     */
    private function savePermalink(PageEntity $page)
    {

        if ($page->parentId == null) {
            $permalink = '/';
        } else {
            $permalink = $page->urlTitle . '/';
            $parent = $this->getPage($page->parentId, $page->siteId);

            if ($parent !== null) {
                $permalink = $parent->permalink . $permalink;
            }

            $permalink = '/' . ltrim($permalink, '/');
        }

        $columnsAndValues = array(
            'permalink' => $permalink,
            'rewriteTo' => 'page/' . $page->id,
            'siteId' => $page->siteId
        );

        if ($page->rewriteId === null) {
            $this->pdo->insert('rewrite', $columnsAndValues);

            $lastInsertId = $this->pdo->lastInsertId();
            $this->pdo->update('page', array('rewriteId' => $lastInsertId), 'id = ?', $page->id);
        } else {
            $this->pdo->update('rewrite', $columnsAndValues, 'id = ?', $page->rewriteId);
        }

    }
}